Skip to content

Bug-hunt round 33: Ctrl+C self-exit misdiagnosis, dead API, doc gaps - #50

Closed
REPPL wants to merge 5 commits into
mainfrom
bughunt-33
Closed

Bug-hunt round 33: Ctrl+C self-exit misdiagnosis, dead API, doc gaps#50
REPPL wants to merge 5 commits into
mainfrom
bughunt-33

Conversation

@REPPL

@REPPL REPPL commented Aug 7, 2026

Copy link
Copy Markdown
Owner

Round 33 of the autonomous bug-hunt loop (state: issue #24).

Confirmed substantive (1)

record's <-ctx.Done() shutdown branch misdiagnosed (or silently swallowed) a recorder that self-exited just before the interrupt.

internal/record/record.go's <-ctx.Done() case called finaliseOutputs on every child unconditionally, unlike the sibling anyExit branch (fixed in round 32, PR #48), which excludes a self-exited child from the sweep before stopAll runs. When a recorder's done channel closed in the (empirically measured, sub-millisecond) window before the interrupt reached the select, that recorder fell through to finaliseOutputs exactly like a normally-stopped one:

  • with no artefact, it was diagnosed via classifyMissingOutput's "stayed blocked on the permission prompt" narrative — disproved by its own exit, with the real exit status never surfaced (internal/record/tcc.go:116-129);
  • with a partial artefact (e.g. a mid-session device loss leaving a truncated audio.wav), finaliseOutputs saw usable data, appended no problem, and Run returned nil — a truncated recording silently presented as a clean, complete session.

Both failure shapes were empirically reproduced during verification (200/200 and 60/60 trials against the pre-fix code), and the defect directly contradicts the documented contract at docs/reference/cli.md:127-130 and the code's own stated invariant (internal/record/record.go, the comment above the anyExit branch's exclusion logic).

Fix: factored the pre-stopAll early-exit sampling both branches need into a shared sampleEarlyExits helper (internal/record/record.go), used by both select cases, so a recorder that exits on its own gets the same classifyRecorderExit diagnosis regardless of which case observes the exit. Two new regression tests (internal/record/record_test.go: TestCtrlCDiagnosesRecorderThatSelfExitedWithNoOutput, TestCtrlCDiagnosesRecorderThatSelfExitedWithPartialOutput) were watched to fail against the pre-fix code and pass after.

Confirmed nitpicks (4)

  • AGENTS.md (CLAUDE.md is a symlink to it) listed go test ./... as a CI gate distinct from go test -race ./... and claimed "CI runs every gate above"; .github/workflows/ci.yml and release.yml in fact run only the race-enabled line. Corrected the claim; kept the plain line as a documented local-only command.
  • docs/reference/session-directory.md's words omission-reasons row didn't mention that a word with empty, whitespace-only, or invisible-only text is also dropped (internal/transcribe/transcribe.go:529-537), unlike the equivalent text-row rule documented one row up. Re-examined for present-day accuracy independent of origin — round 31 split-discarded a similarly framed finding on the different question of whether round 30 introduced fresh staleness; this round's two independent refuters both confirmed the row itself is incomplete regardless of when the behaviour was introduced.
  • The same page's timeline.jsonl field table had no Required column and did not state t's (or a speech payload's t1's) ±1e9-second magnitude bound, which internal/timeline/timeline.go's ReadEntries enforces — the same bound the transcript.jsonl table documents for t0/t1 three sections above.
  • The same page's utt-003 example trimmed the illustrated utterance text to start mid-sentence ("Now I expect…") while keeping the untrimmed fixture's t0 and word times, leaving the first shown word 1.6s after its own t0. Restored the example's opening words so they agree with t0 again.
  • internal/analyze/validate.go's exported Validate function had zero callers anywhere in the module — Ingest uses the unexported validate/indexTimeline pair directly, and internal/review calls analyze.Load, not Validate. Being under internal/, it cannot have external consumers either. Removed, along with the now-unused errors import.

Considered and refuted

  • A claimed round-32 regression desynchronising timeline.jsonl from analyze's emitted analysis request via HTML-escaping (internal/session/session.go's jsonlEncoder vs internal/analyze/emit.go's plain json.Marshal): one refuter built HEAD and HEAD~1 and proved EmitRequest's output byte-identical across round 32's change — it re-decodes the timeline and re-marshals, so WriteJSONL's encoder choice never reaches it, and the asymmetry actually pre-dates round 32 via session.SafeText. Split verdict, discarded.
  • An inverted transcription segment span (whisperx.go/whispercpp.go accepting end < start): the downstream t1→t0 clamp is a documented, deliberate fallback (docs/reference/session-directory.md:70, internal/timeline/timeline.go), so no real misbehaviour survives.
  • A CI cross-compile comment's CGO-flag mismatch (.github/workflows/ci.yml's comment vs its actual Build step flags): real discrepancy, no behavioural consequence — the repo has no cgo-conditional source.
  • docs/reference/cli.md:152's "ingest reads timeline.jsonl only" phrasing, read against ingest's own findings.jsonl verdict-guard scan: the same paragraph states that guard explicitly four sentences later; no reader is misled.
  • docs/reference/session-directory.md's manifest.json example omitting optional fields present in the real fixture: both omitted fields are marked optional in the table directly above; not misleading.

Verification

Every finding faced two independent adversarial refuters before being included above; only findings both refuters failed to kill were fixed. All four sweep dimensions (code, docs-vs-functionality, infrastructure, internal doc consistency) also surfaced several items that survived one but not both refuters — those are omitted per the loop's "when in doubt, discard" rule and not itemised here.

Gates

go build, gofmt -l ., go vet ./..., go test ./..., go test -race ./..., the pipeline smoke (merge + report against examples/sample-session), and sh -n install.sh && bash -n install.sh all pass on the branch head.

.abcd/work/DECISIONS.md gains the round's entry in this PR.

REPPL added 5 commits August 7, 2026 13:37
record's <-ctx.Done() shutdown branch called finaliseOutputs on every
child unconditionally, unlike the sibling anyExit branch (fixed in
round 32), which excludes a self-exited child from the sweep. When a
recorder's done channel closed in the sub-millisecond window before
the interrupt reached the select, that recorder fell through to
finaliseOutputs and was either misdiagnosed via classifyMissingOutput
("stayed blocked on the permission prompt") when it captured nothing,
or silently exited 0 with a truncated recording presented as a clean
session when it left partial data.

Factor the pre-stopAll early-exit sampling into a shared
sampleEarlyExits helper used by both select cases, so a recorder that
exits on its own gets the same classifyRecorderExit diagnosis no
matter which case observes the exit.

Assisted-by: Claude:claude-sonnet-5
AGENTS.md listed go test ./... and go test -race ./... as two
separate gates and said CI runs every gate above; CI and release.yml
both run only the race-enabled line. Note the local-only line and
narrow the CI claim to match (CLAUDE.md is a symlink to AGENTS.md, so
one edit covers both).

Assisted-by: Claude:claude-sonnet-5
Three gaps against the actual code and fixture: the words
omission-reasons list did not mention that empty, whitespace-only, or
invisible-only text is dropped (transcribe.go already documents this
for segment text one row up); the timeline.jsonl field table had no
Required column and did not state t's (and payload t1's) ±1e9-second
bound, which ReadEntries enforces; and the utt-003 example trimmed the
utterance text to start at "Now" while keeping t0 and the real word
times from the untrimmed fixture, leaving the first shown word 1.6s
after its own t0. Restore the example's opening words so t0 matches
its first word again.

Assisted-by: Claude:claude-sonnet-5
Validate had zero callers anywhere in the module — ingest uses the
unexported validate/indexTimeline pair directly, and internal/review
calls analyze.Load, not Validate. Being under internal/, it cannot
have external consumers either. Drop it along with the now-unused
errors import.

Assisted-by: Claude:claude-sonnet-5
Assisted-by: Claude:claude-sonnet-5
@REPPL REPPL closed this Aug 7, 2026

REPPL commented Aug 7, 2026

Copy link
Copy Markdown
Owner Author

Closing: this branch (bughunt-33) collided with another session's concurrently-run round 33, which merged as #49 first. Superseded by a rebased PR from bughunt-34 with the round renumbered and the now-redundant AGENTS.md fix (already applied by #49) dropped.


Assisted-by: Claude:claude-sonnet-5


Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant